import { isFunction, isObject, isString } from "./typeUtils"

type Rule = {
  alias: string,
  convert: <T>(val: T, obj: object) => unknown,
  globals?: <T>(val: T, obj: object) => object
}
type Rules = Array<{[key: string]: string | Function | Rule} | string>
type Options = {
  allowNull: boolean
}

const defaultOptions: Options = {
  allowNull: true,
}

function toRuleObject(rules: Rules): {[key: string]: Rule} {
  return rules.reduce((obj, rule) => {
    if (isString(rule)) {
      const key = rule as string
      obj[key] = {alias: key, convert: (val) => val} as Rule
      return obj
    }
    Object.keys(rule).forEach((key) => {
      const val = (rule as any)[key]

      if (isString(val)) {
        obj[key] = {alias: val, convert: (val) => val} as Rule
        return
      }

      if (isFunction(val)) {
        obj[key] = {alias: key, convert: val} as Rule
        return
      }

      if (isObject(val)) {
        obj[key] = val
        return
      }

      return obj
    })

    return obj
  }, {} as any)
}

export function transform<T>(source: object, rules: Rules, options?: Options): T {
  if (!isObject(source)) {
    throw new Error('source is not an object')
  }

  options = {...defaultOptions, ...options}
  
  const ruleObject = toRuleObject(rules)
  return Object.keys(ruleObject).reduce((obj, key) => {
    const rule = ruleObject[key]!
    const value = rule.convert((source as any)[key], source)
    if (options.allowNull || value != null) {
      obj[rule.alias] = value
    }
    if (rule.globals) {
      const global = rule.globals(value, source)
      obj = {
        ...obj,
        ...global,
      }
    }
    return obj
  }, {} as any)
}